﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net.Mail;
using System.Threading;
using System.Threading.Tasks;
using Reactor.Environment;
using Reactor.Messaging;
using Reactor.Messaging.ServiceBus;
using Reactor.ServiceGrid.ServiceEntities;
using Reactor.Services.AdvisoryEmailService.Data;

namespace Reactor.Services.AdvisoryEmailService
{
    public class Service : ManagedReactorService
    {
        #region Fields

        private Thread _emailThread;
        private AdvisoryChannelToEmailMapProvider _mapProvider;
        private readonly List<AdvisoryPublicationToEmailMap> _maps = new List<AdvisoryPublicationToEmailMap>();
        private Timer _timer;
        private readonly BlockingCollection<EmailTask> _taskQueue = new BlockingCollection<EmailTask>();
        private readonly object _mapSync = new object();

        #endregion

        #region Overrides of ManagedReactorService

        protected override void OnStarted()
        {
            // We can be reasonably certain the connection data referenced here will be 
            //  present, as the platform will not allow the service to be run without it.
            _mapProvider = new AdvisoryChannelToEmailMapProvider(Context.ServiceBus.Configuration.ConnectionData.DatabaseConnectionString);

            // Main thread to monitor the task queue.
            _emailThread = new Thread(BeginMonitoringTaskQueue);
            _emailThread.Start();

            _timer = new Timer(RefreshIntervalElapsed);
            var refreshInterval = GetRefreshInterval();
            _timer.Change(refreshInterval, refreshInterval);

            Task.Factory.StartNew(RefreshMaps);
        }

        protected override void OnStopping()
        {
            base.OnStopping();

            _emailThread.Abort();
            Log.Debug("Internal task processor thread aborted.");

            _timer.Dispose();
            Log.Debug("Refresh timer disposed.");

            lock (_mapSync)
                _maps.Clear();
        }

        #endregion

        #region Public Methods

        public void EnqueueEmailTask(EmailTask emailTask)
        {
            _taskQueue.Add(emailTask);
        }

        public AdvisoryPublicationToEmailMap GetEmailMap(string advisoryTopic)
        {
            lock (_mapSync)
            {
                return _maps.SingleOrDefault(m => m.AdvisoryChannel == advisoryTopic);
            }
        }

        #endregion

        #region Private Methods

        private static TimeSpan GetRefreshInterval()
        {
            TimeSpan interval;
            var configValue = Context.Configuration.GetConfigurationValue("Services.AdvisoryEmailService.MapRefreshInterval");
            return !TimeSpan.TryParse(configValue, out interval) ? TimeSpan.FromMinutes(5) : interval;
        }

        private void SubscribeToAdvisoryTopics()
        {
            var bus = Context.ServiceBus;
            var destinationsRequested = _maps.Select(map => new PubSubChannel(map.AdvisoryChannel)).Cast<IMessagingChannel>().ToList();

            var handlerRegistry = Context.ServiceLocator.GetInstance<IHandlerRegistry>();
            var handlerTypes = handlerRegistry.GetHandlerTypesContainedInAssemblies(new[] {typeof(Service).Assembly});

            foreach (var handlerType in handlerTypes)
            {
                var msgTypeHandled = handlerType.GetDeclaredMessageTypeHandledByConcreteType();
                var destination = destinationsRequested.FirstOrDefault(d => d.DestinationName == msgTypeHandled.FullName);

                bus.Subscribe(msgTypeHandled, new[] {destination});
            }
        }

        private void RefreshIntervalElapsed(object state)
        {
            try
            {
                RefreshMaps();
            }
            catch (Exception e)
            {
                Log.Error("An exception was thrown while attepting to refresh maps.", e);
            }
        }

        private void RefreshMaps()
        {
            var maps = _mapProvider.GetMaps();

            lock (_mapSync)
            {
                foreach (var map in maps)
                {
                    var emailMap = map; // Avoid access to modified closure
                    var existing = _maps.SingleOrDefault(m => m.AdvisoryChannel == emailMap.AdvisoryChannel);
                    if (existing != null)
                        MergeMapWithUpdates(existing, emailMap);
                    else
                        AddMap(emailMap);
                }

                ResolveRemovedMaps(maps);

                SubscribeToAdvisoryTopics();
            }
        }

        private void ResolveRemovedMaps(IEnumerable<AdvisoryPublicationToEmailMap> maps)
        {
            var toBeRemoved = new List<AdvisoryPublicationToEmailMap>();
            // ReSharper disable LoopCanBeConvertedToQuery
            foreach (var map in _maps)
            {
                var emailMap = map; // Avoid access to modified closure
                if(!maps.Any(m => m.AdvisoryChannel == emailMap.AdvisoryChannel))
                    toBeRemoved.Add(emailMap);
            }
            // ReSharper restore LoopCanBeConvertedToQuery

            toBeRemoved.ForEach(m => _maps.Remove(m));
        }

        private void AddMap(AdvisoryPublicationToEmailMap map)
        {
            _maps.Add(map);
        }

        private static void MergeMapWithUpdates(AdvisoryPublicationToEmailMap original, AdvisoryPublicationToEmailMap update)
        {
            if (original == null) throw new ArgumentNullException("original");
            if (update == null) throw new ArgumentNullException("update");

            original.BodyFormatString = update.BodyFormatString;
            original.Cc = update.Cc;
            original.SendTo = update.SendTo;
            original.SubjectFormatString = update.SubjectFormatString;
        }

        private void BeginMonitoringTaskQueue()
        {
            Log.Debug("Internal task processor started.");

            while (!_taskQueue.IsCompleted)
            {
                EmailTask task = null;
                try
                {
                    task = _taskQueue.Take();

                    Log.Debug("Email task accepted.");
                }
                catch (InvalidOperationException) { /* IOE means that Take() was called on a complete collection */ }

                Task.Factory.StartNew(() => SendEmail(task));
            }
        }

        internal static void SendEmail(EmailTask task)
        {
            if(Log.IsDebugEnabled) Log.DebugFormat("Sending advisory email:{0}{1}Subject: {2}{0}{1}To:{3}", System.Environment.NewLine, "\t", task.Subject, task.To);

            var message = new MailMessage {From = new MailAddress(task.From)};

            foreach (var email in task.To)
                message.To.Add(new MailAddress(email));

            foreach (var email in task.Cc)
                message.CC.Add(new MailAddress(email));

            message.Subject = task.Subject;
            message.Body = task.Body;

            try
            {
                var client = new SmtpClient {EnableSsl = true};
                client.Send(message);
            }
            catch (SmtpException smtpe)
            {
                Log.Error("Unable to send advisory email.", smtpe);
            }
        }

        #endregion
    }
}
